#include "extralib.h"

#include <cstring>
#include <memory.h>
#include <cstring>
#include <vector>
#include <algorithm>
#include <string>
#include <sstream>
#include <cstdlib>

// fcuk u, 'microsoft specific'
#ifdef _MSC_VER
#	undef min
#	undef max
#endif

#include "tools/HiveStore.hpp"
#include "tools/PluginData.hpp"
#include "tools/UserData.hpp"
#include "tools/AMXX_Helpers.h"
#include "tools/CopyPaste.hpp"

#define CopyPaste_Node(param_n) \
	t_NodeID handle = params[param_n];\
	t_Node *node = g_Nodes[handle];\
	if(!node)\
	{\
		MF_LogError(amx, AMX_ERR_NATIVE, "Invalid node handle (#%d)", handle);\
		return 0;\
	}\
// end CopyPaste_Node

#define CopyPaste_CurNode \
	if(!g_CurNode)\
	{\
		MF_LogError(amx, AMX_ERR_NATIVE, "Not preparing menu at this moment");\
		return 0;\
	}\
// end CopyPaste_CurNode

namespace MenuNode
{
	const int
		c_CaptionLen =   48,
		c_InfoLen =      48,
		c_DetailsLen =  400,
		c_MenuLen =     512,
		c_MaxPageSize =   7;

	const float c_DefaultLifetime = 120.0f;

	// public flags
	const int
		Flag_Disabled =   (1<<1),
		Flag_Persistent = (1<<2),
		Flag_ShowAll =    (1<<3),
		Flag_ForceMenu =  (1<<4);

	// private flags
	const int
		Flag_TreatAsDisabled = (1<<30);





	typedef int t_NodeID;

	struct t_Node
	{
		int Extra, PageSize, Flags, AccessFlags;
		char Caption[c_CaptionLen];
		t_PluginID Owner;
		std::vector<t_NodeID> Childs;

		t_Node(): Extra(), PageSize(c_MaxPageSize), Flags(), AccessFlags(-1), Owner()
		{memset(Caption, 0, sizeof(Caption));}
	};

	struct t_PluginDataProto
	{
		int onCheckState;
		int onBeforeShown;
		int onSelected;
		bool CallbacksRegistered;
	};

	struct t_UserData
	{
		struct t_MenuStatus
		{
			t_NodeID Handle;
			int Page;
			float ExpirationTime;
		}
		CurrentMenu, NextMenu, *ActiveMenuState;

		t_NodeID ItemMapping[c_MaxPageSize + 1];

		t_UserData(): CurrentMenu(), NextMenu(), ActiveMenuState()
		{memset(ItemMapping, 0, sizeof(ItemMapping));}
	};




	
	t_Node *g_CurNode = nullptr;
	char g_CurInfo[c_InfoLen];
	char g_CurDetails[c_DetailsLen];

	t_HiveStore<t_Node> g_Nodes;
	t_PluginData<t_PluginDataProto> g_Plugins;
	t_UserData g_Users[33];





	void OnAmxxAttach()
	{
	}

	void OnAmxxDetach()
	{
	}

	void hook_ClientCommand(edict_t *pEntity) //// TODO handle user input
	{
		const char* cmd = g_engfuncs.pfnCmd_Argv(0);
		int user = g_engfuncs.pfnIndexOfEdict(pEntity);

		if(!strcmp(cmd, "menuselect"))
		{
			t_UserData &userdata = g_Users[user];

			if(userdata.ActiveMenuState != &userdata.CurrentMenu)
				RETURN_META(MRES_IGNORED);

			userdata.ActiveMenuState = nullptr;

			if(*static_cast<float*>(MF_PlayerPropAddr(user, Player_MenuExpire)))
				RETURN_META(MRES_IGNORED);

			int key = atoi(g_engfuncs.pfnCmd_Argv(1));

			if(key > 0 || key <= c_MaxPageSize)
			{
				if(!userdata.ItemMapping[key])
				{
					userdata.NextMenu = userdata.CurrentMenu;
					userdata.ActiveMenuState = &userdata.NextMenu;
					RETURN_META(MRES_IGNORED);
				}

				t_NodeID handle = userdata.ItemMapping[key];
				t_Node *node = g_Nodes[handle];

				MF_ExecuteForward(g_Plugins[node->Owner]->onCheckState, user, handle, 0);
				if(node->Flags & Flag_Disabled || ((node->Flags & Flag_ForceMenu) && node->Childs.empty()))
					node->Flags |= Flag_TreatAsDisabled;
				else node->Flags &= ~Flag_TreatAsDisabled;

				if(node->Flags = Flag_TreatAsDisabled)
				{
					userdata.NextMenu = userdata.CurrentMenu;
					userdata.ActiveMenuState = &userdata.NextMenu;
					RETURN_META(MRES_IGNORED);
				}

				if(node->Childs.size())
				{
					userdata.ActiveMenuState = &userdata.NextMenu;
					userdata.NextMenu.Handle = handle;
					userdata.NextMenu.Page = 0;
					userdata.NextMenu.ExpirationTime = c_DefaultLifetime;
				}
				else
				if(node->Flags & Flag_Persistent)
					userdata.ActiveMenuState = &userdata.NextMenu;

				MF_ExecuteForward(g_Plugins[node->Owner]->onSelected, user, handle, 0);
				RETURN_META(MRES_IGNORED);
			}

			if(key == 8 || key == 9)
			{
				userdata.NextMenu = userdata.CurrentMenu;
				userdata.ActiveMenuState = &userdata.NextMenu;
				userdata.NextMenu.Page += (key == 8) ? -1 : 1;
				RETURN_META(MRES_IGNORED);
			}
		}
	}

	void hook_PlayerPostThink(edict_t *pEntity)
	{
		static int user;
		user = g_engfuncs.pfnIndexOfEdict(pEntity);

		if(g_Users[user].ActiveMenuState != &g_Users[user].NextMenu)
			RETURN_META(MRES_IGNORED);
		
		t_UserData &userdata = g_Users[user];
		userdata.CurrentMenu = userdata.NextMenu;
		userdata.ActiveMenuState = nullptr;

		t_NodeID handle = userdata.CurrentMenu.Handle;
		t_Node *node = g_Nodes[handle];

		MF_ExecuteForward(g_Plugins[node->Owner]->onCheckState, user, handle, 0);
		if(node->Flags & Flag_Disabled || node->Childs.empty())
			RETURN_META(MRES_IGNORED);

		//// TODO optimize
		for(int index = 0, total = node->Childs.size(); index < total; index++)
		{
			t_NodeID child_handle = node->Childs[index];
			t_Node *child_node = g_Nodes[child_handle];

			MF_ExecuteForward(g_Plugins[child_node->Owner]->onCheckState, user, child_handle, handle);

			if(child_node->Flags & Flag_Disabled || ((child_node->Flags & Flag_ForceMenu) && child_node->Childs.empty()))
				child_node->Flags |= Flag_TreatAsDisabled;
			else child_node->Flags &= ~Flag_TreatAsDisabled;
		}

		std::vector<t_NodeID> childs; // actual childs
		childs.reserve(node->Childs.size());
		if(node->Flags & Flag_ShowAll)
			childs.assign(node->Childs.begin(), node->Childs.end());
		else
		{
			for(int index = 0, total = node->Childs.size(); index < total; index++)
			{
				t_NodeID child_handle = node->Childs[index];
				t_Node *child_node = g_Nodes[child_handle];

				if(!(child_node->Flags & Flag_TreatAsDisabled))
					childs.push_back(child_handle);
			}
		}

		int &page = userdata.CurrentMenu.Page;
		int num_pages = (childs.size() - 1) / node->PageSize + 1;

		if(page >= num_pages)
			page = num_pages - 1;
		else
		if(page < 0)
			page = 0;

		int first_child_index = page * node->PageSize;
		int last_child_index = std::min(first_child_index + node->PageSize - 1, int(childs.size()) - 1);

		memset(userdata.ItemMapping, 0, sizeof(userdata.ItemMapping));
		for(int child_index = first_child_index, item = 1; child_index <= last_child_index; child_index++, item++)
			userdata.ItemMapping[item] = childs[child_index];

		g_CurInfo[0] = 0;
		g_CurDetails[0] = 0;

		g_CurNode = node;
		MF_ExecuteForward(g_Plugins[node->Owner]->onBeforeShown, user, handle, 0);
		g_CurNode = nullptr;

		for(int child_index = first_child_index; child_index <= last_child_index; child_index++)
		{
			t_NodeID child_handle = childs[child_index];
			t_Node *child_node = g_Nodes[child_handle];

			MF_ExecuteForward(g_Plugins[child_node->Owner]->onBeforeShown, user, child_handle, handle);
		}

		// Build menu content.
		std::string menu_text; menu_text.reserve(c_MenuLen * 2);

		// 1. Write items.
		{
			std::ostringstream menu_writer(menu_text);

			menu_writer << "\\y" << node->Caption << "\n\\w";

			if(g_CurInfo[0])
				menu_writer << g_CurInfo << "\n";

			menu_writer << "\n";

			for(int child_index = first_child_index, item = 1; child_index <= last_child_index; child_index++, item++)
			{
				t_NodeID child_handle = childs[child_index];
				t_Node *child_node = g_Nodes[child_handle];

				bool disabled = (child_node->Flags & Flag_TreatAsDisabled) != 0;
				menu_writer << (disabled ? "\\d":"\\y") << item << ". ";
				menu_writer << (disabled ? "\\d":"\\w") << child_node->Caption << "\n";
			}

			menu_writer << "\n";

			if(num_pages)
			{
				menu_writer << (!page ? "\\d":"\\y") << "<8";
				menu_writer << "\\y|";
				menu_writer << ((page >= (num_pages - 1)) ? "\\d":"\\y") << "9> ";
			}
			menu_writer << "\\y[0]\n";

			menu_writer << "\n";

			if(g_CurDetails[0])
				menu_writer << "\\w" << g_CurDetails;
		}

		// 2. Remove reduntand color tags.
		{
			std::string buffer; buffer.reserve(menu_text.size());
			const char *src = menu_text.c_str();
			char prev_colortag = 0;

			while(*src)
			{
				if(src[0] == '\\' && (src[1] == 'w' || src[1] == 'd' || src[1] == 'y' || src[1] == 'r'))
				{
					if(prev_colortag == src[1])
					{
						src += 2;
						continue;
					}
					prev_colortag = src[1];
				}
				src++;
			}
			menu_text = buffer;
		}

		userdata.ActiveMenuState = &userdata.CurrentMenu;
		*static_cast<float*>(MF_PlayerPropAddr(user, Player_MenuExpire)) = 0.0f;

		// send menu
		{
			int bytes_to_send = menu_text.length();
			const char *src = menu_text.c_str();
			edict_t *pEdict = g_engfuncs.pfnPEntityOfEntIndex(user);
			char time = char(std::min(120.f, userdata.CurrentMenu.ExpirationTime - gpGlobals->time));

			while(bytes_to_send)
			{
				int bytes_sending = std::min(bytes_to_send, 175);
				bytes_to_send -= bytes_sending;////

				MESSAGE_BEGIN(MSG_ONE, g_Messages.ShowMenu, NULL, pEdict);
				WRITE_SHORT(-1); // slots
				WRITE_CHAR(time);
				WRITE_BYTE(bytes_to_send ? TRUE : FALSE);
				WRITE_STRING(src);
				MESSAGE_END();
			}
		}

		RETURN_META(MRES_IGNORED);
	}





	//native MenuNode:MenuNode_New(const caption[] = "", flags = 0, access_flags = -1);
	EXPORT_NATIVE(MenuNode_New)
	{
		t_Node *node;
		int handle = g_Nodes.Insert(&node);

		const char * caption = MF_GetAmxString(amx, params[1], 0, nullptr);
		strncpy(node->Caption, caption, c_CaptionLen - 1);

		node->Flags = params[2];
		node->AccessFlags = params[3];

		t_PluginID plugin_id = MF_FindScriptByAmx(amx);
		t_PluginDataProto *plugindata = g_Plugins[plugin_id];
		if(!plugindata->CallbacksRegistered)
		{
			plugindata->CallbacksRegistered = true;
			plugindata->onCheckState =  MF_RegisterSPForwardByName(amx, "MenuNode_onCheckState",  FP_CELL, FP_CELL, FP_CELL, FP_DONE);
			plugindata->onBeforeShown = MF_RegisterSPForwardByName(amx, "MenuNode_onBeforeShown", FP_CELL, FP_CELL, FP_CELL, FP_DONE);
			plugindata->onSelected =    MF_RegisterSPForwardByName(amx, "MenuNode_onSelected",    FP_CELL, FP_CELL, FP_CELL, FP_DONE);
		}
		node->Owner = plugin_id;

		return handle;
	}

	//native MenuNode_AddChild(MenuNodeLparent, MenuNode:child);
	EXPORT_NATIVE(MenuNode_AddChild)
	{
		t_Node *parent;
		{
			CopyPaste_Node(1);
			parent = node;
		}

		int child_handle;
		{
			CopyPaste_Node(2);
			child_handle = handle;
		}

		parent->Childs.push_back(child_handle);

		return 0;
	}

	//native MenuNode_SetCaption(MenuNode:handle, const caption[] = "");
	EXPORT_NATIVE(MenuNode_SetCaption)
	{
		CopyPaste_Node(1);
		strncpy(node->Caption, MF_GetAmxString(amx, params[2], 0, nullptr), c_CaptionLen - 1);
		return 0;
	}

	//native MenuNode_SetExtra(MenuNode:handle. any:value);
	EXPORT_NATIVE(MenuNode_SetExtra)
	{
		CopyPaste_Node(1);
		node->Extra = params[2];
		return 0;
	}

	//native any:MenuNode_GetExtra(MenuNode:handle);
	EXPORT_NATIVE(MenuNode_GetExtra)
	{
		CopyPaste_Node(1);
		return node->Extra;
	}

	//native MenuNode_SetFlags(MenuNode:handle. flags, BitwiseOperator:op = Bitwise_Overwrite);
	EXPORT_NATIVE(MenuNode_SetFlags)
	{
		CopyPaste_Node(1);

		int op_arg = params[2];
		int op = params[3];
		switch(op)
		{
		case Bitwise_Overwrite:
			node->Flags = op_arg;
			break;
		case Bitwise_AND:
			node->Flags &= op_arg;
			break;
		case Bitwise_OR:
			node->Flags |= op_arg;
			break;
		case Bitwise_XOR:
			node->Flags ^= op_arg;
			break;
		default:
			MF_LogError(amx, AMX_ERR_NATIVE, "Invalid bitwise operator specified");
		}

		return 0;
	}

	//native MenuNode_GetFlags(MenuNode:handle);
	EXPORT_NATIVE(MenuNode_GetFlags)
	{
		CopyPaste_Node(1);
		return node->Flags;
	}

	//native MenuNode_SetPageSize(MenuNode:handle, items_per_page = 7);
	EXPORT_NATIVE(MenuNode_SetPageSize)
	{
		CopyPaste_Node(1);

		int page_size = params[2];
		if(page_size < 1 || page_size > c_MaxPageSize)
		{
			MF_LogError(amx, AMX_ERR_NATIVE, "Page size out of range (%d)", page_size);
			return 0;
		}
		node->PageSize = page_size;

		return 0;
	}

	//native MenuNode_SetInfo(const info[]);
	EXPORT_NATIVE(MenuNode_SetInfo)
	{
		CopyPaste_CurNode;
		strncpy(g_CurInfo, MF_GetAmxString(amx, params[1], 0, nullptr), c_InfoLen - 1);
		return 0;
	}

	//native MenuNode_SetDetails(const details[]);
	EXPORT_NATIVE(MenuNode_SetDetails)
	{
		CopyPaste_CurNode;
		strncpy(g_CurDetails, MF_GetAmxString(amx, params[1], 0, nullptr), c_DetailsLen - 1);
		return 0;
	}

	//native MenuNode_Show(user, MenuNode:handle, page = 0, lifetime = 9000);
	EXPORT_NATIVE(MenuNode_Show)
	{
		CopyPaste_User(1);
		CopyPaste_Node(2);

		int page = params[3];
		if(page < 0)
		{
			MF_LogError(amx, AMX_ERR_NATIVE, "Negative page index (%d)", page);
			return 0;
		}

		int lifetime = params[4];
		if(lifetime < 1)
		{
			MF_LogError(amx, AMX_ERR_NATIVE, "Zero or negative lifetime (%d seconds)", lifetime);
			return 0;
		}

		if(node->Childs.empty())
			return 0;

		t_UserData &userdata = g_Users[user];
		userdata.ActiveMenuState = &userdata.NextMenu;
		userdata.NextMenu.Handle = handle;
		userdata.NextMenu.Page = page;
		userdata.NextMenu.ExpirationTime = gpGlobals->time + float(lifetime);

		return 0;
	}





	AMX_NATIVE_INFO ExportNatives[] =
	{
		EXPORT_PAIR(MenuNode_New),
		EXPORT_PAIR(MenuNode_AddChild),
		EXPORT_PAIR(MenuNode_SetCaption),
		EXPORT_PAIR(MenuNode_SetExtra),
		EXPORT_PAIR(MenuNode_GetExtra),
		EXPORT_PAIR(MenuNode_SetFlags),
		EXPORT_PAIR(MenuNode_GetFlags),
		EXPORT_PAIR(MenuNode_SetPageSize),
		EXPORT_PAIR(MenuNode_SetInfo),
		EXPORT_PAIR(MenuNode_SetDetails),
		EXPORT_PAIR(MenuNode_Show),
		{nullptr, nullptr}
	};
}